"use client"; /** * /w/:workspace/settings/teams — teamspaces. * * - List/create: GET/POST /workspaces/:id/teams * - Rename/update: PATCH /workspaces/:id/teams/:teamId * - Delete: DELETE /workspaces/:id/teams/:teamId * - Add member: POST /workspaces/:id/teams/:teamId/members { userId } * - Remove member: DELETE /workspaces/:id/teams/:teamId/members/:userId * * Teams (teamspaces) organize the sidebar's "Teamspaces" section. Only * owner/admin manage them (server-side enforcement is authoritative). * * NOTE FOR THE CORE AGENT: the teams list/get endpoints return only * `memberCount` (Prisma `_count`) — no endpoint returns the per-team member * *list* (TEAM_SELECT excludes the `members` relation), so this page can add * members but cannot display them individually. When `GET …/teams/:teamId` * (or the list) includes `members`, the member list + per-member remove can be * rendered here (the DELETE …/members/:userId route already exists). */ import { useState } from "react"; import { useParams } from "next/navigation"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Loader2, Plus, Trash2, Users } from "lucide-react"; import { toast } from "@/stores/ui-store"; import { apiFetch } from "@/lib/api"; import { useWorkspace, useWorkspaceMembers } from "@/hooks/use-workspace"; import { SettingsShell, Section, Field, inputCls } from "@/components/settings/settings-shell"; interface Team { id: string; name: string; slug?: string | null; description?: string | null; color?: string | null; isPrivate?: boolean; /** The teams API returns only a member count, not the member list. */ memberCount?: number; } function unwrapTeams(body: unknown): Team[] { const b = body as { data?: Team[] } | Team[] | null; if (Array.isArray(b)) return b; if (b && typeof b === "object" && Array.isArray((b as { data?: Team[] }).data)) return (b as { data: Team[] }).data; return []; } export default function SettingsTeamsPage() { const params = useParams<{ workspace: string }>(); const slugOrId = params?.workspace ?? ""; const { data: ws, isLoading } = useWorkspace(slugOrId); const wid = ws?.id ?? ""; const qc = useQueryClient(); const teams = useQuery({ queryKey: ["workspaces", wid, "teams"], queryFn: async (): Promise => unwrapTeams(await apiFetch(`/workspaces/${wid}/teams`)), enabled: !!wid }); const [name, setName] = useState(""); const [color, setColor] = useState(""); const create = useMutation({ mutationFn: (input: { name: string; color?: string }) => apiFetch(`/workspaces/${wid}/teams`, { method: "POST", body: input }), onSuccess: () => { qc.invalidateQueries({ queryKey: ["workspaces", wid, "teams"] }); setName(""); setColor(""); } }); const role = (ws?.role ?? "").toLowerCase(); const canManage = role === "owner" || role === "admin"; const submitCreate = () => { if (!name.trim()) return; create.mutate( { name: name.trim(), color: color || undefined }, { onSuccess: () => toast("success", "Team created"), onError: (e) => toast("error", "Create failed", (e as Error).message) } ); }; return ( {isLoading || !ws ? (

Loading…

) : (
{canManage && (
setName(e.target.value)} onKeyDown={(e) => e.key === "Enter" && submitCreate()} /> setColor(e.target.value)} aria-label="Color" />
)}
{teams.isLoading &&

Loading…

}
    {(teams.data ?? []).map((t) => ( qc.invalidateQueries({ queryKey: ["workspaces", wid, "teams"] })} /> ))}
{teams.data && teams.data.length === 0 && (

No teams yet. Create one to organize shared work.

)}
)}
); } function TeamCard({ team, workspaceId, canManage, onManage }: { team: Team; workspaceId: string; canManage: boolean; onManage: () => void }) { const [editing, setEditing] = useState(false); const [name, setName] = useState(team.name); const [description, setDescription] = useState(team.description ?? ""); const [addingId, setAddingId] = useState(""); const [busy, setBusy] = useState(false); const members = useWorkspaceMembers(workspaceId); const allMembers = (members.data ?? []) as { userId: string; user: { id: string; name: string; email: string; image?: string | null } }[]; const save = async () => { setBusy(true); try { await apiFetch(`/workspaces/${workspaceId}/teams/${team.id}`, { method: "PATCH", body: { name: name.trim(), description: description || null } }); toast("success", "Team updated"); setEditing(false); onManage(); } catch (e) { toast("error", "Update failed", (e as Error).message); } finally { setBusy(false); } }; const remove = async () => { setBusy(true); try { await apiFetch(`/workspaces/${workspaceId}/teams/${team.id}`, { method: "DELETE" }); toast("success", "Team deleted"); onManage(); } catch (e) { toast("error", "Delete failed", (e as Error).message); setBusy(false); } }; const addMember = async () => { if (!addingId) return; setBusy(true); try { await apiFetch(`/workspaces/${workspaceId}/teams/${team.id}/members`, { method: "POST", body: { userId: addingId } }); toast("success", "Member added"); setAddingId(""); onManage(); } catch (e) { toast("error", "Add failed", (e as Error).message); } finally { setBusy(false); } }; return (
  • {editing ? ( setName(e.target.value)} aria-label="Team name" /> ) : (

    {team.name}

    )} {!editing && canManage && ( )} {!editing && canManage && ( )}
    {editing && (
    setDescription(e.target.value)} />
    )} {team.description && !editing ?

    {team.description}

    : null} {/* members */}

    {team.memberCount != null ? `${team.memberCount} member${team.memberCount === 1 ? "" : "s"}` : "Members"}

    {canManage ? (
    ) : (

    The teams API doesn't return the individual member list yet, so members can't be displayed or removed here.

    )}
  • ); }